﻿using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Moonlite.Modules.Editors.ImageViewer.Dds
{
    /// <summary>
    /// Represents a class that can read and write images that have been compressed using the DirectDraw Surface image compression method.
    /// </summary>
    public static class DdsImageLoader
    {
        [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
        private static extern IntPtr MemCopy(IntPtr dest, IntPtr src, UIntPtr count);

        /// <summary>
        /// Reads a <see cref="DdsPixelFormat"/> data structure from the specified <see cref="System.IO.BinaryReader"/>.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.BinaryWriter"/> to read from.</param>
        /// <returns>The read <see cref="DdsPixelFormat"/>.</returns>
        private static DdsPixelFormat ReadPixelFormat(BinaryReader stream)
        {
            DdsPixelFormat pixelFormat = new DdsPixelFormat();
            pixelFormat.dwSize = stream.ReadUInt32();
            pixelFormat.dwFlags = stream.ReadUInt32();
            pixelFormat.dwFourCC = stream.ReadUInt32();
            pixelFormat.dwRGBBitCount = stream.ReadUInt32();
            pixelFormat.dwRBitMask = stream.ReadUInt32();
            pixelFormat.dwGBitMask = stream.ReadUInt32();
            pixelFormat.dwBBitMask = stream.ReadUInt32();
            pixelFormat.dwAlphaBitMask = stream.ReadUInt32();
            return pixelFormat;
        }

        /// <summary>
        /// Writes the specified <see cref="DdsPixelFormat"/> data structure to the specified <see cref="System.IO.BinaryWriter"/>.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.BinaryWriter"/> to write to.</param>
        /// <param name="pixelFormat">The <see cref="DdsPixelFormat"/> data structure to write.</param>
        private static void WritePixelFormat(BinaryWriter stream, DdsPixelFormat pixelFormat)
        {
            stream.Write(pixelFormat.dwSize);
            stream.Write(pixelFormat.dwFlags);
            stream.Write(pixelFormat.dwFourCC);
            stream.Write(pixelFormat.dwRGBBitCount);
            stream.Write(pixelFormat.dwRBitMask);
            stream.Write(pixelFormat.dwGBitMask);
            stream.Write(pixelFormat.dwBBitMask);
            stream.Write(pixelFormat.dwAlphaBitMask);
        }

        /// <summary>
        /// Reads a <see cref="DdsHeader"/> data structure from the specified <see cref="System.IO.BinaryReader"/>.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.BinaryReader"/> to read from.</param>
        /// <returns>The read <see cref="DdsHeader"/>.</returns>
        private static DdsHeader ReadHeader(BinaryReader stream)
        {
            DdsHeader header = new DdsHeader();
            header.dwSize = stream.ReadUInt32();
            header.dwFlags = stream.ReadUInt32();
            header.dwHeight = stream.ReadUInt32();
            header.dwWidth = stream.ReadUInt32();
            header.dwPitchOrLinearSize = stream.ReadUInt32();
            header.dwDepth = stream.ReadUInt32();
            header.dwMipMapCount = stream.ReadUInt32();
            header.dwReserved1 = new uint[11];
            for (int i = 0; i < 11; i++)
            {
                header.dwReserved1[i] = stream.ReadUInt32();
            }

            header.dwPixelFormat = ReadPixelFormat(stream);
            header.dwCaps1 = stream.ReadUInt32();
            header.dwCaps2 = stream.ReadUInt32();
            header.dwCaps3 = stream.ReadUInt32();
            header.dwCaps4 = stream.ReadUInt32();
            header.dwReserved2 = stream.ReadUInt32();
            return header;
        }

        /// <summary>
        /// Writes the specified <see cref="DdsHeader"/> data structure to the specified <see cref="System.IO.BinaryWriter"/>.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.BinaryWriter"/> to write to.</param>
        /// <param name="header">The <see cref="DdsHeader"/> data structure to write.</param>
        private static void WriteHeader(BinaryWriter stream, DdsHeader header)
        {
            stream.Write(header.dwSize);
            stream.Write(header.dwFlags);
            stream.Write(header.dwHeight);
            stream.Write(header.dwWidth);
            stream.Write(header.dwPitchOrLinearSize);
            stream.Write(header.dwDepth);
            stream.Write(header.dwMipMapCount);
            for (int i = 0; i < 11; i++)
            {
                stream.Write(header.dwReserved1[i]);
            }

            WritePixelFormat(stream, header.dwPixelFormat);
            stream.Write(header.dwCaps1);
            stream.Write(header.dwCaps2);
            stream.Write(header.dwCaps3);
            stream.Write(header.dwCaps4);
            stream.Write(header.dwReserved2);
        }

        /// <summary>
        /// Shifts the specified mask.
        /// </summary>
        /// <param name="mask">The mask.</param>
        /// <returns>The result of the shift.</returns>
        private static int Shift(uint mask)
        {
            int result = 32;
            while (mask != 0)
            {
                result--;
                mask <<= 1;
            }

            return result;
        }

        /// <summary>
        /// Converts the <see cref="System.String"/> to an <see cref="System.IntPtr"/>.
        /// </summary>
        /// <param name="input">The input <see cref="System.String"/>.</param>
        /// <returns>The converted <see cref="System.String"/>.</returns>
        private static IntPtr ConvertStringToIntPtr(string input)
        {
            return Marshal.StringToHGlobalAnsi(input);
        }

        /// <summary>
        /// Converts the <see cref="System.String"/> to an <see cref="System.UInt32"/>.
        /// </summary>
        /// <param name="input">The input <see cref="System.String"/>.</param>
        /// <returns>The converted <see cref="System.String"/></returns>
        private static uint ConvertStringToUnsignedInteger32(string input)
        {
            byte[] bytes = Encoding.ASCII.GetBytes(input);
            return BitConverter.ToUInt32(bytes, 0);
        }

        /// <summary>
        /// Reads DirectDraw Surface image data from the specified <see cref="System.IO.BinaryReader"/> to the specified <see cref="System.Drawing.Bitmap"/>.
        /// </summary>
        /// <param name="reader">The <see cref="System.IO.BinaryReader"/>.</param>
        /// <param name="header">The <see cref="DdsHeader"/>.</param>
        /// <param name="image">The <see cref="System.Drawing.Bitmap"/>.</param>
        /// <returns>Whether the reading was succesful.</returns>
        private unsafe static bool ReadData(BinaryReader reader, DdsHeader header, out Bitmap image)
        {
            image = new Bitmap(8, 8);
            uint flags = header.dwPixelFormat.dwFlags;
            if ((flags & (uint)DdsPixelFormatFlags.DDPF_FOURCC) != 0)
            {
                uint dxt1 = ConvertStringToUnsignedInteger32("DXT1");
                uint dxt2 = ConvertStringToUnsignedInteger32("DXT5");
                if (header.dwPixelFormat.dwFourCC == dxt1)
                {
                    image = DdsDxt.LoadDxt1(reader, (int)header.dwWidth, (int)header.dwHeight);
                }

                if (header.dwPixelFormat.dwFourCC == dxt2)
                {
                    image = DdsDxt.LoadDxt5(reader, (int)header.dwWidth, (int)header.dwHeight);
                }
            }

            int redShift = Shift(header.dwPixelFormat.dwRBitMask);
            int greenShift = Shift(header.dwPixelFormat.dwGBitMask);
            int blueShift = Shift(header.dwPixelFormat.dwBBitMask);
            int alphaShift = 0;
            if ((header.dwPixelFormat.dwFlags & (uint)DdsPixelFormatFlags.DDPF_ALPHAPIXELS) != 0)
            {
                alphaShift = Shift(header.dwPixelFormat.dwAlphaBitMask);
            }

            if ((flags & (uint)DdsPixelFormatFlags.DDPF_RGB) != 0)
            {
                if (header.dwPixelFormat.dwRGBBitCount != 32)
                {
                    return false;
                }

                if ((header.dwPixelFormat.dwFlags & (uint)DdsPixelFormatFlags.DDPF_ALPHAPIXELS) != 0)
                {
                    image = new Bitmap((int)header.dwWidth, (int)header.dwHeight, PixelFormat.Format32bppArgb);
                }
                else
                {
                    image = new Bitmap((int)header.dwWidth, (int)header.dwHeight, PixelFormat.Format32bppRgb);
                }

                for (int x = 0; x < header.dwWidth; x++)
                {
                    for (int y = 0; y < header.dwHeight; y++)
                    {
                        uint color = reader.ReadUInt32();
                        int red = (int)((color & header.dwPixelFormat.dwRBitMask) >> redShift);
                        int green = (int)((color & header.dwPixelFormat.dwGBitMask) >> greenShift);
                        int blue = (int)((color & header.dwPixelFormat.dwBBitMask) >> blueShift);
                        int alpha = (int)((color & header.dwPixelFormat.dwAlphaBitMask) >> alphaShift);
                        image.SetPixel(x, y, Color.FromArgb(alpha, red, green, blue));
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Reads the specified <see cref="System.IO.Stream"/> and creates a <see cref="System.Drawing.Bitmap"/> using the DirectDraw Surface image compression algorithm.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.Stream"/>.</param>
        /// <returns>The created <see cref="System.Drawing.Bitmap"/>.</returns>
        public static Bitmap Read(Stream stream)
        {
            bool dummy = true;
            return Read(stream, out dummy);
        }

        /// <summary>
        /// Reads the specified <see cref="System.IO.Stream"/> and creates a <see cref="System.Drawing.Bitmap"/> using the DirectDraw Surface image compression algorithm.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.Stream"/>.</param>
        /// <param name="result">A value indicating whether the reading was succesful.</param>
        /// <returns>The created <see cref="System.Drawing.Bitmap"/>.</returns>
        public static Bitmap Read(Stream stream, out bool result)
        {
            using (BinaryReader reader = new BinaryReader(stream))
            {
                uint magic = reader.ReadUInt32();
                DdsHeader header = ReadHeader(reader);
                Bitmap image = null;
                result = ReadData(reader, header, out image);
                return image;
            }
        }

        /// <summary>
        /// Writes the specified <see cref="System.Drawing.Bitmap"/> to the specified <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="stream">The <see cref="System.IO.Stream"/>.</param>
        /// <param name="bitmap">The <see cref="System.Drawing.Bitmap"/>.</param>
        /// <returns>A value indicating whether the writing was sucessful.</returns>
        public static bool Write(Stream stream, Bitmap bitmap)
        {
            BinaryWriter writer = new BinaryWriter(stream);
            writer.Write(Convert.ToUInt32("DDS "));
            DdsHeader header = new DdsHeader();
            header.dwSize = 124;
            header.dwFlags = (uint)(DdsHeaderFlags.DDSD_CAPS | DdsHeaderFlags.DDSD_HEIGHT | DdsHeaderFlags.DDSD_WIDTH | DdsHeaderFlags.DDSD_PIXELFORMAT);
            header.dwHeight = (uint)bitmap.Height;
            header.dwWidth = (uint)bitmap.Width;
            header.dwPitchOrLinearSize = 0;
            header.dwDepth = 0;
            header.dwMipMapCount = 0;
            for (int i = 0; i < 11; i++)
            {
                header.dwReserved1[i] = 0;
            }

            header.dwCaps1 = (uint)DdsCaps1Flags.DDSCAPS_TEXTURE;
            header.dwCaps2 = 0;
            header.dwCaps3 = 0;
            header.dwCaps4 = 0;
            header.dwReserved2 = 0;
            header.dwPixelFormat = new DdsPixelFormat();
            header.dwPixelFormat.dwSize = 32;
            header.dwPixelFormat.dwFlags = (uint)(DdsPixelFormatFlags.DDPF_ALPHAPIXELS | DdsPixelFormatFlags.DDPF_RGB);
            header.dwPixelFormat.dwFourCC = 0;
            header.dwPixelFormat.dwRGBBitCount = 32;
            header.dwPixelFormat.dwAlphaBitMask = 0xFF000000;
            header.dwPixelFormat.dwRBitMask = 0x00FF0000;
            header.dwPixelFormat.dwGBitMask = 0x0000FF00;
            header.dwPixelFormat.dwBBitMask = 0x000000FF;
            WriteHeader(writer, header);
            for (int x = 0; x < bitmap.Width; x++)
            {
                for (int y = 0; y < bitmap.Height; y++)
                {
                    Color pixel = bitmap.GetPixel(x, y);
                    uint color = (uint)((pixel.A << 24) + (pixel.R << 16) + (pixel.G << 8) + pixel.B);
                    writer.Write(color);
                }
            }

            return true;
        }
    }
}
